package org.mbds.wolf.seql;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import org.mbds.wolf.exception.WolfException;
import org.mbds.wolf.seql.exceptions.BadRequestException;
import org.mbds.wolf.seql.exceptions.MalformedConfigFileException;
import org.mbds.wolf.tools.BytesTool;

/**
 * 
 * @author alesas
 *
 */
public class SeqlParser 
{
	
	/**
	 * parseSeql
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 */
	public static Map<String, Object> parseSeql(final String seqlStatement, final File configFile) throws Exception 
	{
		String appletName = SeqlWords.nextWord(seqlStatement, SeqlWords.STR_FROM);
		if (appletName==null)
		{
			appletName = SeqlWords.nextWord(seqlStatement, SeqlWords.STR_INTO);
		}
		if (appletName == null)
		{
			throw new BadRequestException();
		}
		AppletModel appletModel = SeqlMetadata.getInstance(configFile).getAppletByAlias(appletName);
		String seqlInstruction = SeqlWords.nextWord(seqlStatement, "");
		if (!appletModel.checkInstructionsBelongToApplet(seqlInstruction))
		{
			throw new BadRequestException();
		}
//		String type = appletModel.getInstructionType(seqlInstruction);
		// TODO: manage instructions with the SeqlType
		String type = seqlInstruction;
		try
		{
			if (type.equalsIgnoreCase(SeqlWords.SELECT))
			{
				return parseSelect(appletModel, seqlStatement);
			} 
			else if (type.equalsIgnoreCase(SeqlWords.CREATE)) 
			{ 
				return parseCreate(appletModel, seqlStatement);
			} 
			else if (type.equalsIgnoreCase(SeqlWords.INSERT)) 
			{ 
				return parseInsert(appletModel, seqlStatement);
			} 
			else if (type.equalsIgnoreCase(SeqlWords.UPDATE)) 
			{
				return parseUpdate(appletModel, seqlStatement);
			} 
			else if (type.equalsIgnoreCase(SeqlWords.DELETE)) 
			{
				return parseDelete(appletModel, seqlStatement);
			} 
			else if (type.equalsIgnoreCase(SeqlWords.CHECK)) 
			{
				return parseCheck(appletModel, seqlStatement);
			} 
			else 
			{
				return parseExec(appletModel, seqlStatement);
			} 
		} 
		catch (Exception e) 
		{
			throw e;
		}
	} 
	
	/**
	 * parseSelect
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseSelect(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: Select field1, field2, ..., fieldn from AppletAlias [fetch <recordId>]
		String instruction = SeqlWords.nextWord(seqlStatement, "");
 		String fields = SeqlWords.wordBetween(seqlStatement, instruction, SeqlWords.STR_FROM);
		if (fields == null) {
			throw new BadRequestException();
		}
 		fields = fields.trim();
		if (fields.length() == 0) 
		{
			throw new BadRequestException();
		}
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put(SeqlWords.STR_APPLET, appletModel); // Applet model
		
		fields = fields.replaceAll("\\s", "");
 		String[] field_list = fields.split(",");
 		byte[] P1s = appletModel.checkDataBelongToApplet(field_list);
 
 		map.put(SeqlWords.STR_FIELD, field_list); // the fields to be read
 		
 		byte[] CLA_INS = appletModel.getIns(instruction);

 		byte P2 = 0x00;
		String fetch = SeqlWords.nextWord(seqlStatement, SeqlWords.STR_FETCH);
		if (fetch!=null && fetch.trim().length()>0)
		{
			int intVal = Integer.parseInt(fetch.trim());
			P2 = BytesTool.integerToByteArray(intVal, 1)[0];
		}
		int iter = 0;
		byte[][] apdus = new byte[field_list.length][];

		for (String st : field_list) {
			int length = appletModel.getDataLength(st);
			byte[] command = getApduCommand(CLA_INS, P1s[iter], P2, null, length);
			apdus[iter] = command;
			iter++;
		}
		
		map.put(SeqlWords.STR_COMMAND, apdus); // the APDU commands to be transmitted
		return map;
	}
	
	/**
	 * parseInsert
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseInsert(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: Insert into AppletAlias (field1, field2, ..., fieldn) values(val1, val2, ..., valn)
		SeqlWords.checkRequiredWords(seqlStatement, SeqlWords.STR_INTO, SeqlWords.STR_VALUES, SeqlWords.STR_BRACKET_L, SeqlWords.STR_BRACKET_R);
		String instruction = SeqlWords.nextWord(seqlStatement, "");
 		String fields = SeqlWords.wordBetween(seqlStatement, appletModel.getAlias(), SeqlWords.STR_VALUES);
		if (fields == null) {
			throw new BadRequestException();
		}
		fields = fields.replace(SeqlWords.STR_BRACKET_L, "");
		fields = fields.replace(SeqlWords.STR_BRACKET_R, "");
 		fields = fields.trim();
		if (fields.length() == 0) 
		{
			throw new BadRequestException();
		}
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put(SeqlWords.STR_APPLET, appletModel); // Applet model
		
		fields = fields.replaceAll("\\s", "");
 		String[] field_list = fields.split(SeqlWords.STR_COMMA);
 		byte[] P1s = appletModel.checkDataBelongToApplet(field_list);
 
 		map.put(SeqlWords.STR_FIELD, field_list); // the fields to be read
 		
		String values = seqlStatement.substring(seqlStatement.indexOf(SeqlWords.STR_VALUES));
		values = values.replaceAll("\\s", "");
		values = values.substring(values.indexOf(SeqlWords.STR_BRACKET_L) + 1, values.lastIndexOf(SeqlWords.STR_BRACKET_R));
		String[] value_list = values.split(SeqlWords.STR_COMMA);

		if  (field_list.length != value_list.length)
		{
			throw new BadRequestException();
		}
 		
 		byte[] CLA_INS = appletModel.getIns(instruction);
		int iter = 0;
		byte[][] apdus = new byte[value_list.length][];

		for (String val : value_list) {
			int length = appletModel.getDataLength(field_list[iter]);
			byte[] command = getApduCommand(CLA_INS, P1s[iter], (byte) 0x00, val, length);
			apdus[iter] = command;
			iter++;
		}
		
		map.put(SeqlWords.STR_COMMAND, apdus); // the APDU commands to be transmitted
		return map;
	}

	/**
	 * parseUpdate
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseUpdate(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: Update from AppletAlias (field1, field2, ..., fieldn) values(val1, val2, ..., valn) [fetch <recordId>]
		SeqlWords.checkRequiredWords(seqlStatement, SeqlWords.STR_FROM, SeqlWords.STR_VALUES, SeqlWords.STR_BRACKET_L, SeqlWords.STR_BRACKET_R);
		String instruction = SeqlWords.nextWord(seqlStatement, "");
 		String fields = SeqlWords.wordBetween(seqlStatement, appletModel.getAlias(), SeqlWords.STR_VALUES);
		if (fields == null) {
			throw new BadRequestException();
		}
		fields = fields.replace(SeqlWords.STR_BRACKET_L, "");
		fields = fields.replace(SeqlWords.STR_BRACKET_R, "");
 		fields = fields.trim();
		if (fields.length() == 0) 
		{
			throw new BadRequestException();
		}
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put(SeqlWords.STR_APPLET, appletModel); // Applet model
		
		fields = fields.replaceAll("\\s", "");
 		String[] field_list = fields.split(SeqlWords.STR_COMMA);
 		byte[] P1s = appletModel.checkDataBelongToApplet(field_list);
 
 		map.put(SeqlWords.STR_FIELD, field_list); // the fields to be read
 		
		String values = seqlStatement.substring(seqlStatement.indexOf(SeqlWords.STR_VALUES));
		values = values.replaceAll("\\s", "");
		values = values.substring(values.indexOf(SeqlWords.STR_BRACKET_L) + 1, values.lastIndexOf(SeqlWords.STR_BRACKET_R));
		String[] value_list = values.split(SeqlWords.STR_COMMA);

		if  (field_list.length != value_list.length)
		{
			throw new BadRequestException();
		}
 		
 		byte[] CLA_INS = appletModel.getIns(instruction);

 		byte P2 = 0x00;
		String fetch = SeqlWords.nextWord(seqlStatement, SeqlWords.STR_FETCH);
		if (fetch!=null && fetch.trim().length()>0)
		{
			int intVal = Integer.parseInt(fetch.trim());
			P2 = BytesTool.integerToByteArray(intVal, 1)[0];
		}
		int iter = 0;
		byte[][] apdus = new byte[value_list.length][];

		for (String val : value_list) {
			int length = appletModel.getDataLength(field_list[iter]);
			byte[] command = getApduCommand(CLA_INS, P1s[iter], P2, val, length);
			apdus[iter] = command;
			iter++;
		}
		
		map.put(SeqlWords.STR_COMMAND, apdus); // the APDU commands to be transmitted
		return map;
	}

	/**
	 * parseDelete
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseDelete(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: Delete field1, field2, ..., fieldn from AppletAlias [fetch <recordId>]
		String instruction = SeqlWords.nextWord(seqlStatement, "");
 		String fields = SeqlWords.wordBetween(seqlStatement, instruction, SeqlWords.STR_FROM);
		if (fields == null) {
			throw new BadRequestException();
		}
 		fields = fields.trim();
		if (fields.length() == 0) 
		{
			throw new BadRequestException();
		}
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put(SeqlWords.STR_APPLET, appletModel); // Applet model
		
		fields = fields.replaceAll("\\s", "");
 		String[] field_list = fields.split(",");
 		byte[] P1s = appletModel.checkDataBelongToApplet(field_list);
 
 		map.put(SeqlWords.STR_FIELD, field_list); // the fields to be read
 		
 		byte[] CLA_INS = appletModel.getIns(instruction);

 		byte P2 = 0x00;
		String fetch = SeqlWords.nextWord(seqlStatement, SeqlWords.STR_FETCH);
		if (fetch!=null && fetch.trim().length()>0)
		{
			int intVal = Integer.parseInt(fetch.trim());
			P2 = BytesTool.integerToByteArray(intVal, 1)[0];
		}
		int iter = 0;
		byte[][] apdus = new byte[field_list.length][];

		for (String st : field_list) {
			int length = appletModel.getDataLength(st);
			byte[] command = getApduCommand(CLA_INS, P1s[iter], P2, null, length);
			apdus[iter] = command;
			iter++;
		}
		
		map.put(SeqlWords.STR_COMMAND, apdus); // the APDU commands to be transmitted
		return map;
	}

	/**
	 * parseCheck
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseCheck(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: check from AppletAlias params(PIN) values(PinVal) [fetch <recordId>]
		//NB: PIN is managed as a data field and must be described in the config XML metadata... 
		return parseExec(appletModel, seqlStatement);
	}

	/**
	 * parseCreate
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseCreate(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: create from AppletAlias params(field1, field2, ...) [values(Val1, Val2, ...)]
		return parseExec(appletModel, seqlStatement);
	}

	
	/**
	 * parseExec
	 * @param appletModel
	 * @param seqlStatement
	 * @return a map of the instruction fields and their corresponding APDU commands
	 * @throws MalformedConfigFileException
	 */
	public static Map<String, Object> parseExec(final AppletModel appletModel, final String seqlStatement) throws Exception
	{
		//Syntax: [Exec] instruc1, instruc2, ..., instrucN from AppletAlias params(param1, param2, ..., paramN) [values(val1, val2, ..., valn)] [fetchOption]
		SeqlWords.checkRequiredWords(seqlStatement, SeqlWords.STR_FROM, SeqlWords.STR_PARAMS, SeqlWords.STR_BRACKET_L, SeqlWords.STR_BRACKET_R);
		
		String instructions = "";
		if (seqlStatement.trim().regionMatches(true, 1, SeqlWords.EXEC, 1, SeqlWords.EXEC.length()))
		{
			instructions = SeqlWords.wordBetween(seqlStatement, SeqlWords.EXEC, SeqlWords.STR_FROM);
		}
		else
		{
			instructions = seqlStatement.substring(0, seqlStatement.indexOf(SeqlWords.STR_FROM));
		}
				
		instructions = instructions.replaceAll("\\s", "");
		String[] instruction_list = instructions.split(SeqlWords.STR_COMMA);
		
		Map<String, Object> map = new HashMap<String, Object>();
		map.put(SeqlWords.STR_APPLET, appletModel); // Applet model
		
 		String params = SeqlWords.wordBetween(seqlStatement, appletModel.getAlias(), SeqlWords.STR_PARAMS);
		if (params == null) {
			throw new BadRequestException();
		}
		params = params.replace(SeqlWords.STR_BRACKET_L, "");
		params = params.replace(SeqlWords.STR_BRACKET_R, "");
 		params = params.trim();
		if (params.length() == 0) 
		{
			throw new BadRequestException();
		}
		
		params = params.replaceAll("\\s", "");
 		String[] param_list = params.split(SeqlWords.STR_COMMA);
 		byte[] P1s = appletModel.checkDataBelongToApplet(param_list);
 
 		map.put(SeqlWords.STR_PARAMS, param_list); // the fields to be read
 		
		String values = seqlStatement.substring(seqlStatement.indexOf(SeqlWords.STR_BRACKET_R));
		String[] value_list = null;
		
		if (values.trim().regionMatches(true, 1, SeqlWords.STR_VALUES, 1, SeqlWords.STR_VALUES.length()))
		{
			values = values.replaceAll("\\s", "");
			values = values.substring(values.indexOf(SeqlWords.STR_BRACKET_L) + 1, values.lastIndexOf(SeqlWords.STR_BRACKET_R));
			value_list = values.split(SeqlWords.STR_COMMA);
		}

		if  (param_list.length != value_list.length)
		{
			throw new BadRequestException();
		}
 		
 		byte P2 = 0x00;
		String fetch = SeqlWords.nextWord(seqlStatement, SeqlWords.STR_FETCH);
		if (fetch!=null && fetch.trim().length()>0)
		{
			int intVal = Integer.parseInt(fetch.trim());
			P2 = BytesTool.integerToByteArray(intVal, 1)[0];
		}
		
		byte[][] apdus = new byte[value_list.length*instruction_list.length][];
		int iter = 0;
		
		
		for (String instruction : instruction_list)
		{
	 		byte[] CLA_INS = appletModel.getIns(instruction);
	
			for (byte P1 : P1s) {
				int length = appletModel.getDataLength(param_list[iter]);
				String val = null;
				if (value_list != null)
				{
					val = value_list[iter];
				}
				byte[] command = getApduCommand(CLA_INS, P1, (byte) P2, val, length);
				apdus[iter] = command;
				iter++;
			}
		}
		map.put(SeqlWords.STR_COMMAND, apdus); // the APDU commands to be transmitted
		return map;
	}

	/**
	 * getApduCommand
	 * @param ins
	 * @param p1
	 * @param p2
	 * @param data
	 * @param le
	 * @return array byte[] of the APDU command
	 * @throws WolfException
	 */
	public static byte[] getApduCommand(final byte[] ins, final byte p1, final byte p2, final String data, final int le) throws WolfException 
	{
//		if (data!=null)
//		{
//			System.out.println("Data : " + data);
//		}
//
		byte[] dataByte = null;
		// Manage extended APDUs
		//response expected length 0, 1, 2 or 3 bytes
		byte[] lehexa = new byte[0];
		if (le!=0) 
		{
			lehexa = BytesTool.integerToByteArray(le, 3);// => 3 bytes
			if (le<=255)//00 00 FF => 1 byte
			{
				lehexa = new byte[]{lehexa[2]};
			}
			else if (le==256)
			{
				lehexa=new byte[]{0x00}; // As defined in ISO7816-4
			}
			else if (le<=65535) //00 FF FF => 2 bytes
			{
				lehexa=new byte[]{lehexa[1], lehexa[2]};
			} 
			else if (le==65536)
			{
				lehexa = new byte[]{0x00, 0x00};// As defined in ISO7816-4
			}
		}
		//data sent length 0, 1 or 3 bytes
		byte[] lc = new byte[0];
		int length = 4+lc.length+lehexa.length; //at least: cla, ins, p1, p2, lc, le
		boolean hasData = (data!=null && !data.isEmpty())?true :false;
		if (hasData) 
		{
			dataByte = data.getBytes();
			lc = BytesTool.integerToByteArray(dataByte.length, 3); 
			//lc <= 255 / FF
			if (lehexa.length<2) 
			{
				lc=new byte[]{lc[2]};
			//lc 256..65536 / 00 FF FF
			} 
			else 
			{
				lc[0]=0x00;
			}
			length += lc.length;
			length += dataByte.length; 
		}

		byte[] command = new byte[length];
		command[0] = (byte) ins[0];
		command[1] = (byte) ins[1];
		command[2] = (byte) p1;
		command[3] = (byte) p2;
		
		System.arraycopy(lc, 0, command, 4, lc.length);

		if (hasData) 
		{
			System.arraycopy(dataByte, 0, command, 4+lc.length, dataByte.length);
		}

		System.arraycopy(lehexa, 0, command, 4+lc.length+(hasData?dataByte.length:0), lehexa.length);
		
		System.out.println("APDU command: "+BytesTool.byteArrayToHexString(command));
		return command;
	}
}
